package main

import (
	"bufio"
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"go/format"
	"hash"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"text/template"
)

var (
	// walkRootPaths contains the path roots we need
	// to walk through. This is relative to the current
	// directory.
	walkRootPaths = []string{
		"../../flux-core",
		"../../flux",
		"../../include",
		"../../scanner.c",
		"../../Cargo.toml",
		"../../Cargo.lock",
	}

	// walkFluxPath	is the path for the flux stdlib.
	walkFluxPath = "../../../stdlib"
)

// lsFiles returns a list of files that exist on the filesystem that git does not ignore
func lsFiles(path string) ([]string, error) {
	// List files in git's cache, i.e. all files that git knows about
	cached, err := gitListFiles(path, "--cached")
	if err != nil {
		return nil, err
	}
	// List files that exist on the filesystem but have not been added to git
	others, err := gitListFiles(path, "--others")
	if err != nil {
		return nil, err
	}
	// List files that exist on the filesystem but should be ignored
	excluded, err := gitListFiles(path, "--others", "--ignored", "--exclude-standard")
	if err != nil {
		return nil, err
	}
	// List files that have been deleted from the filesystem but not git
	deleted, err := gitListFiles(path, "--deleted")
	if err != nil {
		return nil, err
	}
	// We want cached + others - excluded - deleted
	sort.Strings(excluded)
	sort.Strings(deleted)

	testRegex, err := regexp.Compile("/tests/|/tests.rs|/benches/")
	if err != nil {
		return nil, err
	}

	skip := func(f string) bool {
		return contains(excluded, f) || contains(deleted, f) || testRegex.MatchString(f)
	}
	filtered := make([]string, 0, len(cached)+len(others))
	// add cached
	for _, f := range cached {
		if skip(f) {
			continue
		}
		filtered = append(filtered, f)
	}
	// add others
	for _, f := range others {
		if skip(f) {
			continue
		}
		filtered = append(filtered, f)
	}

	return filtered, nil
}

// contains reports if item exists in set, the set must be sorted
func contains(set []string, item string) bool {
	i := sort.SearchStrings(set, item)
	return i < len(set) && set[i] == item
}

// gitListFiles returns a list of files reported using the 'git ls-files' command
func gitListFiles(path string, flags ...string) ([]string, error) {
	args := make([]string, 0, 4+len(flags))
	args = append(args, "ls-files")
	args = append(args, "--full-name")
	args = append(args, flags...)
	args = append(args, "--")
	args = append(args, path)
	cmd := exec.Command("git", args...)
	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	var files []string
	scanner := bufio.NewScanner(bytes.NewReader(out))
	for scanner.Scan() {
		files = append(files, scanner.Text())
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return files, nil
}

// getLibraryFiles will retrieve a list of all of the
// files that the libflux package depends on.
func getLibraryFiles() ([]string, error) {
	var allFiles []string
	for _, root := range walkRootPaths {
		files, err := lsFiles(root)
		if err != nil {
			return nil, err
		}
		allFiles = append(allFiles, files...)
	}

	files, err := lsFiles(walkFluxPath)
	if err != nil {
		return nil, err
	}

	for _, file := range files {
		if !strings.HasSuffix(file, ".flux") {
			continue
		}
		allFiles = append(allFiles, file)
	}
	return allFiles, nil
}

func getRootDir() string {
	cwd, err := os.Getwd()
	if err != nil {
		panic(err)
	}

	for cwd != "/" {
		gitdir := filepath.Join(cwd, ".git")
		if _, err := os.Stat(gitdir); err == nil {
			return cwd
		}
		cwd = filepath.Dir(cwd)
	}
	panic(".git directory not found")
}

func hexdigest(h hash.Hash) string {
	return hex.EncodeToString(h.Sum(nil))
}

func writeTemplate(root string, files []string) error {
	t := template.New("").Funcs(template.FuncMap{
		"hexdigest": func(path string) (string, error) {
			f, err := os.Open(filepath.Join(root, path))
			if err != nil {
				return "", err
			}
			defer func() { _ = f.Close() }()

			shasum := sha256.New()
			if _, err := io.Copy(shasum, f); err != nil {
				return "", err
			}
			return hexdigest(shasum), nil
		},
	})
	t = template.Must(t.Parse(`
// Generated by buildinfo
//
// DO NOT EDIT!

package libflux

// sourceHashes is the hash of the build sources for
// the rust components used by cgo.
// This gets generated from the libflux sources
// and forces the cgo library to rebuild and relink
// the sources. This is because non-C/C++ sources
// are not tracked by Go's build system.'
//lint:ignore U1000 generated code
var sourceHashes = map[string]string{
{{range .}}"{{.}}": "{{hexdigest .}}",
{{end}}
}
`))

	var buf bytes.Buffer
	if err := t.Execute(&buf, files); err != nil {
		return err
	}

	buildinfo, err := format.Source(buf.Bytes())
	if err != nil {
		return err
	}
	return ioutil.WriteFile("buildinfo.gen.go", buildinfo, 0644)
}

func main() {
	root := getRootDir()

	files, err := getLibraryFiles()
	if err != nil {
		panic(err)
	}
	sort.Strings(files)

	if err := writeTemplate(root, files); err != nil {
		panic(err)
	}
}
